import ast
import copy

from ply import yacc

from common.spel_lexer import *
from log import LOGGER
from spel4ast import py_adapter

THIS_VAR = "this"

FINAL_RES = "__final_result"


def p_stmt(p):
    """
    stmt : expr
        | empty
    """
    extra_stmts = p.parser.context.complement_inc_dec_cmd()
    if extra_stmts:
        stmts = [
            ast.Assign(targets=[ast.Name(id=FINAL_RES, ctx=ast.Store())], value=p[1])
        ]
        stmts.extend(extra_stmts)
        p[0] = ast.Module(body=stmts, type_ignores=[])
    else:
        p[0] = ast.Expression(p[1])


def p_stmt_assign(p):
    """
    stmt : assign_stmt
    """
    stmts = [p[1]]
    stmts.extend(p.parser.context.complement_inc_dec_cmd())
    p[0] = ast.Module(body=stmts, type_ignores=[])


def reset_assign_ctx(ast_node):
    if isinstance(ast_node, (ast.Name, ast.Subscript, ast.Attribute)):
        ast_node.ctx = ast.Store()


def p_assign_stmt(p):
    """
    assign_stmt : expr ASSIGN expr
    """
    reset_assign_ctx(p[1])
    p[0] = ast.Assign(targets=[p[1]], value=p[3])


def p_expr(p):
    """
    expr : arith_expr
        | unary_expr
        | rel_expr
        | tri_expr
        | elvis_expr
        | list_def
        | dict_def
        | indexer
        | LPAR expr RPAR
        | var_def
        | property_def
        | bean_def
        | attr_def
        | meth_call
        | func_call
        | literal_def
        | project_expr
        | select_expr
        | type_ref_def
    """
    if len(p) == 2:
        p[0] = p[1]
    else:
        p[0] = p[2]


def p_select_expr(p):
    """
    select_expr : expr member_ref SELECT rel_expr RSQUARE
    """
    p[0] = ast.ListComp(
        elt=ast.Name(id=THIS_VAR, ctx=ast.Load()),
        generators=[
            ast.comprehension(
                target=ast.Name(id=THIS_VAR, ctx=ast.Store()),
                iter=py_adapter.build_call_func_node(
                    py_adapter.BuiltinFunctions.get_iterable.__name__, [p[1]]
                ),
                ifs=[p[4]],
                is_async=0,
            )
        ],
    )


def p_project_expr(p):
    """
    project_expr : expr member_ref PROJECT expr RSQUARE
    """
    p[0] = ast.ListComp(
        elt=p[4],
        generators=[
            ast.comprehension(
                target=ast.Name(id=THIS_VAR, ctx=ast.Store()),
                iter=py_adapter.build_call_func_node(
                    py_adapter.BuiltinFunctions.get_iterable.__name__, [p[1]]
                ),
                ifs=[],
                is_async=0,
            )
        ],
    )


def p_bean_def(p):
    """
    bean_def : BEAN_REF ID
    """


def p_elvis_expr(p):
    """
    elvis_expr : expr ELVIS expr
    """
    p[0] = ast.IfExp(
        test=ast.Compare(
            left=p[1], ops=[ast.NotEq()], comparators=[ast.Constant(None)]
        ),
        body=p[1],
        orelse=p[3],
    )


def p_tri_expr(p):
    """
    tri_expr : expr QMARK expr COLON expr
    """
    p[0] = ast.IfExp(test=p[1], body=p[3], orelse=p[5])


CMP_OP_MAPPING = {
    "==": ast.Eq(),
    "eq": ast.Eq(),
    "!=": ast.NotEq(),
    "ne": ast.NotEq(),
    ">": ast.Gt(),
    "gt": ast.Gt(),
    ">=": ast.GtE(),
    "ge": ast.GtE(),
    "<": ast.Lt(),
    "lt": ast.Lt(),
    "<=": ast.LtE(),
    "le": ast.LtE(),
}


def p_rel_expr_compare(p):
    """
    rel_expr : expr EQ expr
            | expr NE expr
            | expr LE expr
            | expr LT expr
            | expr GE expr
            | expr GT expr

    """
    p[0] = ast.Compare(
        left=p[1], ops=[CMP_OP_MAPPING[p[2].lower()]], comparators=[p[3]]
    )


BOOL_OP_MAPPING = {"&&": ast.And(), "and": ast.And(), "||": ast.Or(), "or": ast.Or()}


def p_rel_expr_bool(p):
    """
    rel_expr : expr AND expr
            | expr OR expr
    """
    p[0] = ast.BoolOp(op=BOOL_OP_MAPPING[p[2].lower()], values=[p[1], p[3]])


def p_rel_expr_unary(p):
    "rel_expr : NOT expr"
    p[0] = ast.UnaryOp(op=ast.Not(), operand=p[2])


def p_rel_expr_match(p):
    """
    rel_expr : expr REG_MATCH expr
    """
    p[0] = ast.Call(
        func=ast.Name(id="match_whole", ctx=ast.Load()), args=[p[3], p[1]], keywords=[]
    )


def p_rel_expr_instof(p):
    """
    rel_expr : expr INST_OF type_ref_def
    """
    p[0] = ast.Call(
        func=ast.Name(id="isinstance", ctx=ast.Load()), args=[p[1], p[3]], keywords=[]
    )


def p_rel_expr_between(p):
    """
    rel_expr : expr BETWEEN LCURLY expr COMMA expr RCURLY
    """
    p[0] = ast.BoolOp(
        op=ast.And(),
        values=[
            ast.Compare(left=p[1], ops=[ast.GtE()], comparators=[p[4]]),
            ast.Compare(left=p[1], ops=[ast.LtE()], comparators=[p[6]]),
        ],
    )


BIN_OP_MAPPING = {
    "+": ast.Add(),
    "-": ast.Sub(),
    "*": ast.Mult(),
    "%": ast.Mod(),
    "mod": ast.Mod(),
    "^": ast.Pow(),
}


def p_arith_expr(p):
    """
    arith_expr : expr PLUS expr
            | expr MINUS expr
            | expr MULT expr
            | expr DIV expr
            | expr MOD expr
            | expr POWER expr
            | expr INC
            | expr DEC
    """
    if len(p) == 4:
        op_code = p[2].lower()
        if op_code in ("/", "div"):
            p[0] = py_adapter.build_call_func_node(
                py_adapter.BuiltinFunctions.cstyle_div.__name__, [p[1], p[3]]
            )
        else:
            p[0] = ast.BinOp(p[1], BIN_OP_MAPPING[op_code], p[3])
    else:
        p[0] = p[1]
        p.parser.context.add_inc_dec_node(p[1], p[2])


UNARY_OP_MAPPING = {"++": ast.Add(), "--": ast.Sub()}


def p_unary_expr_inc_dec(p):
    """
    unary_expr : INC expr %prec UINC
                | DEC expr %prec UDEC
    """
    p[0] = ast.BinOp(left=p[2], op=UNARY_OP_MAPPING[p[1]], right=ast.Constant(1))
    p.parser.context.add_inc_dec_node(p[2], p[1])


def p_unary_expr_uminus(p):
    """
    unary_expr : MINUS expr %prec UMINUS
    """
    p[0] = ast.UnaryOp(op=ast.USub(), operand=p[2])


def p_literal_def_null(p):
    """
    literal_def : NULL
    """
    p[0] = ast.Constant(value=None)


def p_literal_def_true(p):
    """
    literal_def : TRUE
    """
    p[0] = ast.Constant(value=True)


def p_literal_def_false(p):
    """
    literal_def : FALSE
    """
    p[0] = ast.Constant(value=False)


def p_literal_def_str(p):
    """
    literal_def : STR
    """
    p[0] = ast.Constant(value=p[1])


def p_literal_def_const(p):
    """
    literal_def : INTEGER
            | FLOATING
    """
    p[0] = ast.Constant(value=p[1])


def p_indexer(p):
    """
    indexer : list_def LSQUARE expr RSQUARE
            | dict_def  LSQUARE expr RSQUARE
            | var_def LSQUARE expr RSQUARE
            | attr_def LSQUARE expr RSQUARE
            | indexer LSQUARE expr RSQUARE
    """
    p[0] = ast.Subscript(value=p[1], slice=ast.Index(p[3]), ctx=ast.Load())


def p_indexer_str(p):
    "indexer : STR LSQUARE expr RSQUARE"
    p[0] = ast.Subscript(
        value=ast.Constant(p[1]), slice=ast.Index(p[3]), ctx=ast.Load()
    )


def p_dict_def(p):
    """
    dict_def : LCURLY kv_list RCURLY
    """
    p[0] = ast.Dict(keys=list(p[2].keys()), values=list(p[2].values()))


def p_kv_list(p):
    """
    kv_list : expr COLON expr
           | kv_list COMMA expr COLON expr
    """
    if len(p) == 4:
        p[0] = {p[1]: p[3]}
    else:
        p[1][p[3]] = p[5]
        p[0] = p[1]


def p_list_def(p):
    """
    list_def : LCURLY expr_list RCURLY
    """
    p[0] = ast.List(elts=p[2], ctx=ast.Load())


def p_func_call(p):
    """
    func_call : HASH ID args
    """
    p[0] = ast.Call(func=ast.Name(id=p[2], ctx=ast.Load()), args=p[3], keywords=[])


def p_meth_call(p):
    """
    meth_call : expr member_ref ID args
    """
    owner_node = py_adapter.build_call_func_node(
        py_adapter.BuiltinFunctions.implicit_convert.__name__, [p[1]]
    )
    call_node = ast.Call(
        func=ast.Attribute(value=owner_node, attr=p[3], ctx=ast.Load()),
        args=p[4],
        keywords=[],
    )
    if p[2] == ".":
        p[0] = call_node
    else:
        p[0] = ast.IfExp(
            test=ast.Compare(
                left=p[1], ops=[ast.NotEq()], comparators=[ast.Constant(None)]
            ),
            body=call_node,
            orelse=ast.Constant(None),
        )


def p_type_ref_def(p):
    """
    type_ref_def : TYPE_REF LPAR cls_def RPAR
    """
    p[0] = ast.Name(".".join(p[3]), ctx=ast.Load())


def p_cls_def(p):
    """
    cls_def : ID
            | cls_def DOT ID
    """
    if len(p) == 2:
        p[0] = [p[1]]
    else:
        p[1].append(p[3])
        p[0] = p[1]


def p_args(p):
    "args : LPAR expr_list RPAR"
    p[0] = p[2]


def p_expr_list_many(p):
    """
    expr_list : expr_list COMMA expr
    """
    p[1].append(p[3])
    p[0] = p[1]


def p_expr_list_single(p):
    """
    expr_list : expr
    """
    p[0] = [p[1]]


def p_expr_list_empty(p):
    """
    expr_list : empty
    """
    p[0] = []


def p_property_def(p):
    "property_def : ID"
    p[0] = ast.Attribute(
        value=ast.Name(id=THIS_VAR, ctx=ast.Load()), attr=p[1], ctx=ast.Load()
    )


def p_var_def(p):
    """
    var_def : HASH ID
    """
    p.parser.context.add_owner(p[2])
    p[0] = ast.Name(id=p[2], ctx=ast.Load())


def p_attr_def(p):
    """
    attr_def : expr member_ref ID
    """
    p.parser.context.add_member(p[3])
    attr_node = ast.Attribute(value=p[1], attr=p[3], ctx=ast.Load())
    if p[2] == ".":
        p[0] = attr_node
    else:
        p[0] = ast.IfExp(
            test=ast.Compare(
                left=p[1], ops=[ast.NotEq()], comparators=[ast.Constant(None)]
            ),
            body=attr_node,
            orelse=ast.Constant(None),
        )


def p_member_ref(p):
    """
    member_ref : DOT
            | SAFE_NAVI
    """
    p[0] = p[1]


def p_empty(p):
    "empty :"
    pass


def p_error(p):
    err_msg = "Syntax error at EOF"
    if p:
        err_msg = "Syntax error at '%s', line:%d, pos:%d" % (
            p.value,
            p.lineno,
            p.lexpos,
        )
    raise SyntaxError(err_msg)


class ParseContext:
    def __init__(self):
        self.errors = []
        self.vars = []
        self.inc_dec_nodes = []
        self.cur_pos = None

    def add_inc_dec_node(self, node, op):
        new_node = copy.copy(node)
        reset_assign_ctx(new_node)
        self.inc_dec_nodes.append((new_node, op))

    def complement_inc_dec_cmd(self):
        return [
            ast.AugAssign(target=node, op=UNARY_OP_MAPPING[op], value=ast.Constant(1))
            for node, op in self.inc_dec_nodes
        ]

    def add_owner(self, var):
        self.vars.append([var])
        self.cur_pos = self.vars[-1]

    def add_member(self, member):
        self.cur_pos.append(member)

    def get_vars(self):
        return [".".join(x) for x in self.vars]

    def get_owners(self):
        return [x[0] for x in self.vars]

    def collect_error(self, msg):
        self.errors.append(msg)

    def has_error(self):
        return len(self.errors) != 0

    def get_errors(self):
        return ";".join(self.errors)

    def clear(self):
        self.errors.clear()
        self.vars.clear()
        self.inc_dec_nodes.clear()


class SpelInterpreter(object):
    def __init__(self):
        self.lexer = lex.lex()
        self.parser = yacc.yacc()
        # 这里的self.parser就是p.parser，可以把自己的ParseContext塞进去，记录必要的信息
        self.parser.context = ParseContext()

    def parse_i(self, code):
        self.parser.context.clear()
        tree = None
        try:
            # 这里需指定lexer
            tree = self.parser.parse(code, lexer=self.lexer)
        except SyntaxError as e:
            err_msg = "parse spel:%s failed, err:%s" % (code, str(e))
            LOGGER.error(err_msg)
            self.parser.context.collect_error(err_msg)
        return self.parser.context, tree

    def parse(self, code):
        return self.parse_i(code)[0]

    def run(self, code, run_ctx: dict = None):
        code = code.strip()
        if not code:
            raise SyntaxError("code empty!")

        ctx, tree = self.parse_i(code)
        if not tree:
            LOGGER.error("syntax err:%s" % ctx.get_errors())
            raise SyntaxError(ctx.get_errors())

        LOGGER.info(ast.dump(tree))
        ast.fix_missing_locations(tree)

        if run_ctx is None:
            run_ctx = {}
        py_adapter.prepare_builtin_funcs(run_ctx)

        if isinstance(tree, ast.Expression):
            bc = compile(tree, filename="<ast>", mode="eval")
            return eval(bc, run_ctx)
        elif isinstance(tree, ast.Module):
            bc = compile(tree, filename="<ast>", mode="exec")
            exec(bc, run_ctx)
            if run_ctx is not None and FINAL_RES in run_ctx:
                return run_ctx.get(FINAL_RES)
            else:
                return None
        else:
            raise SyntaxError("code:%s Should be Expression or Module!" % (code))


class SpelTest(unittest.TestCase):
    def test_spel_yacc(self):
        interp = SpelInterpreter()
        self.assertEqual(
            interp.parse("'reqVO'+#reqVO+#reqVO.getCacheLan()").get_vars(),
            ["reqVO", "reqVO"],
        )
        self.assertFalse(
            interp.parse("'reqVO'+#reqVO+#reqVO.getCacheLan()").has_error()
        )
        self.assertEqual(
            interp.parse("'reqVO'+#reqVO+#reqVO.orderId").get_vars(),
            ["reqVO", "reqVO.orderId"],
        )
        self.assertEqual(
            interp.parse("#result.code!='0' or #result1?.data.size()==0").get_vars(),
            ["result.code", "result1.data"],
        )
        self.assertEqual(interp.parse("#a == 1 and #b eq 2").get_vars(), ["a", "b"])
        self.assertEqual(interp.parse("#a == 1 AND #b EQ 2").get_vars(), ["a", "b"])
        self.assertEqual(
            interp.parse("#a between {1,2} && #b eq 2").get_vars(), ["a", "b"]
        )
        self.assertEqual(interp.parse("2*-#a-5").get_vars(), ["a"])
        self.assertEqual(interp.parse("#l.size()").get_vars(), ["l"])
        self.assertEqual(interp.parse("'hello'[0]").get_vars(), [])
        self.assertEqual(interp.parse("'hello'[0]").get_vars(), [])
        self.assertEqual(
            interp.parse("{'a':{1,2,3}, 'b':{1,2}}['a'][2]").get_vars(), []
        )
        self.assertEqual(interp.parse("#a=1").get_vars(), ["a"])
        self.assertEqual(interp.parse("#a.b=1").get_vars(), ["a.b"])
        self.assertEqual(interp.parse("#a[1]=1").get_vars(), ["a"])
        self.assertEqual(interp.parse("2=1").get_vars(), [])
        self.assertEqual(interp.parse("2 div 10.0").get_vars(), [])
        self.assertEqual(interp.parse("2 Div 10.0").get_vars(), [])
        self.assertEqual(interp.parse("7 mod 3").get_vars(), [])
        self.assertEqual(interp.parse("@userBean.scope").get_vars(), [])
        self.assertEqual(interp.parse("#user.getName().b").get_vars(), ["user.b"])
        self.assertEqual(interp.parse("{'a', 1, 3.0}[0]").get_vars(), [])
        self.assertEqual(interp.parse("#param?.tag").get_vars(), ["param.tag"])
        self.assertEqual(interp.parse("#param?.tag()").get_vars(), ["param"])
        self.assertEqual(interp.parse("#doSth(#param?.tag)").get_vars(), ["param.tag"])
        self.assertEqual(
            interp.parse("T(com.JsonUtil).getJsonString(#param?.tag)").get_vars(),
            ["param.tag"],
        )
        self.assertEqual(
            interp.parse("T(com.JsonUtil).getJsonString(#param)").get_vars(), ["param"]
        )
        self.assertEqual(
            interp.parse("#json.getJsonString(#param?.tag)").get_vars(),
            ["json", "param.tag"],
        )

    def test_ast_example(self):
        tree = ast.parse("make_str('e')", mode="eval")
        print("ast type:%s, ast:%s" % (type(tree).__name__, ast.dump(tree)))

    def test_spel_run_assign(self):
        interp = SpelInterpreter()
        run_ctx = {}
        self.assertEqual(interp.run("#a=1", run_ctx), None)
        self.assertEqual(run_ctx.get("a"), 1)

        run_ctx = {"a": [1, 2, 3]}
        self.assertEqual(interp.run("#a[1]=1", run_ctx), None)
        self.assertEqual(run_ctx.get("a")[1], 1)
        self.assertEqual(run_ctx.get("a"), [1, 1, 3])

        self.assertEqual(interp.run("{1,2,3}[1]=1"), None)

        run_ctx = {"a": {"b": 1, "c": 2}}
        self.assertEqual(interp.run("#a['c']=10", run_ctx), None)
        self.assertEqual(run_ctx.get("a")["c"], 10)

    def test_spel_run_normal(self):
        interp = SpelInterpreter()
        self.assertEqual(interp.run("1+2"), 3)
        self.assertEqual(interp.run("1-2"), -1)
        self.assertEqual(interp.run("3*2"), 6)
        self.assertEqual(interp.run("5/2"), 2)
        self.assertEqual(interp.run("5/2+4"), 6)
        self.assertEqual(interp.run("5*2+4-5/2"), 12)
        self.assertEqual(interp.run("5*2+4-5%2"), 13)
        self.assertEqual(interp.run("7%3"), 1)
        self.assertEqual(interp.run("7 mod 3"), 1)
        self.assertEqual(interp.run("5.1 + 4.9"), 10.0)
        self.assertEqual(interp.run("5 + 4.9"), 9.9)
        self.assertEqual(interp.run("5/2.0+4-3.2"), 3.3)
        self.assertEqual(interp.run("5/2.0+4-3.2+2.5^2"), 9.55)
        self.assertEqual(interp.run("10"), 10)
        self.assertEqual(interp.run("'a'"), "a")
        self.assertEqual(interp.run("'a' + 'b'"), "ab")
        self.assertEqual(interp.run("'ac' + 'b'"), "acb")
        self.assertEqual(interp.run("7^2"), 49)
        self.assertEqual(interp.run("1==2"), False)
        self.assertEqual(interp.run("2==2 && 3==3 and 4==4"), True)
        self.assertEqual(interp.run("2==2 && 3==3 and 4==4 and false"), False)
        self.assertEqual(interp.run("1==2 || 3>2"), True)
        self.assertEqual(interp.run("2 ne 2 or 3<=2"), False)
        self.assertEqual(interp.run("not (2 ne 2) or 3<=2"), True)
        self.assertEqual(interp.run("'abc'[2]"), "c")
        self.assertEqual(interp.run("'hello'[0]"), "h")
        self.assertEqual(interp.run("1==2?10: 9"), 9)
        self.assertEqual(interp.run("1!=2?10: 9"), 10)
        self.assertEqual(interp.run("1?:0"), 1)
        self.assertEqual(interp.run("null?:0"), 0)
        self.assertEqual(interp.run("#a+1", {"a": 10}), 11)
        self.assertEqual(interp.run("-#a+1", {"a": 10}), -9)
        self.assertEqual(interp.run("2+-#a", {"a": 10}), -8)
        self.assertEqual(interp.run("#a[1]", {"a": [1, 5, 6]}), 5)
        self.assertEqual(interp.run("#a[1]*2", {"a": [1, 5, 6]}), 10)
        self.assertEqual(interp.run("#a[1]", {"a": "hello"}), "e")
        self.assertEqual(interp.run("#a['e']", {"a": {"e": 4}}), 4)
        self.assertEqual(interp.run("#a+1 between {11, 12}", {"a": 10}), True)
        self.assertEqual(interp.run("#a between {11, 12}", {"a": 10}), False)
        self.assertTrue(interp.run("'hello' instanceof T(str)"))
        self.assertTrue(interp.run("1 instanceof T(int)"))
        self.assertFalse(interp.run("{1,2} instanceof T(str)"))
        self.assertTrue(interp.run("'hello' matches '[a-z]+'"))
        self.assertFalse(interp.run("#a matches '[a-z]+'", {"a": "abc1"}))

        class SubStru:
            def __init__(self):
                self.e = "hello"

        class Stru:
            def __init__(self):
                self.b = 4
                self.c = SubStru()

        self.assertEqual(interp.run("#a.b-2", {"a": Stru()}), 2)
        self.assertEqual(interp.run("#a.c.e[2]", {"a": Stru()}), "l")

    def test_spel_run_member_call(self):
        interp = SpelInterpreter()
        self.assertEqual(interp.run("'hello'.length()+1"), 6)
        self.assertEqual(interp.run("#len('hello')+2"), 7)
        self.assertEqual(interp.run("#len({1,2,3})+1"), 4)
        self.assertEqual(interp.run("'hello'.substring(1,2)"), "e")
        self.assertEqual(interp.run("'hello'.substring(1)"), "ello")
        self.assertEqual(interp.run("'hello'.toUpperCase()"), "HELLO")
        self.assertEqual(interp.run("'HELLO'.toLowerCase()"), "hello")

    def test_spel_run_collection(self):
        interp = SpelInterpreter()
        self.assertEqual(interp.run("{1,2}"), [1, 2])
        self.assertEqual(interp.run("{1,2} + {3, 4}"), [1, 2, 3, 4])
        self.assertEqual(interp.run("{1:'a',2:'b'}"), {1: "a", 2: "b"})
        self.assertEqual(interp.run("{1:'a',2:'b'}[2]"), "b")
        self.assertEqual(interp.run("{1, 'a', 4}[1]"), "a")
        self.assertEqual(interp.run("{'a':{1,2,3}, 'b':{1,2}}['a'][2]"), 3)
        self.assertEqual(interp.run("{1,2}.![#this+1]"), [2, 3])
        self.assertEqual(interp.run("{1,2,3,4}.?[#this%2==0]"), [2, 4])
        self.assertEqual(interp.run("{1,2}?.![#this+1]"), [2, 3])
        self.assertEqual(interp.run("{1,2,3,4}?.?[#this%2==0]"), [2, 4])

        class Stru:
            def __init__(self, value):
                self.value = value

            def __eq__(self, other):
                if isinstance(other, Stru):
                    return self.value == other.value
                else:
                    return False

        l = [Stru(1), Stru(2), Stru(3), Stru(4)]
        self.assertEqual(interp.run("#l.![#this.value+1]", {"l": l}), [2, 3, 4, 5])
        self.assertEqual(
            interp.run("#l?.?[#this.value%2==0]", {"l": l}), [Stru(2), Stru(4)]
        )

        l = [Stru(1), Stru(2), Stru(3), Stru(4), Stru(5), Stru(6)]
        self.assertEqual(
            interp.run("#l?.?[#this.value%2==0].![#this.value]", {"l": l}), [2, 4, 6]
        )
        self.assertEqual(
            interp.run(
                "#l?.?[#this.value%2==0 && #this.value%3==1].![#this.value]", {"l": l}
            ),
            [4],
        )
        self.assertEqual(
            interp.run(
                "#l?.?[#this.value%2==0 && #this.value%3==1 or #this.value%3==0].![#this.value]",
                {"l": l},
            ),
            [3, 4, 6],
        )

        self.assertEqual(interp.run("{1:'a',2:'b'}.![#this.key]"), [1, 2])
        self.assertEqual(
            interp.run("{1:'a',2:'b'}.?[#this.key%2==0].![#this.value]"), ["b"]
        )
        self.assertEqual(interp.run("{1:'a',2:'b'}.![#this.value]"), ["a", "b"])
        self.assertEqual(
            interp.run("{1:'a',2:'b'}.?[#this.value == 'a'].![#this.key]"), [1]
        )

        self.assertEqual(interp.run("{1:'a',2:'b'}.![key]"), [1, 2])
        self.assertEqual(interp.run("{1:'a',2:'b'}.?[key%2==0].![value]"), ["b"])
        self.assertEqual(interp.run("{1:'a',2:'b'}.![value]"), ["a", "b"])
        self.assertEqual(interp.run("{1:'a',2:'b'}.?[value == 'a'].![key]"), [1])
        self.assertEqual(
            interp.run("#m.?[value == 'a'].![key]", {"m": {1: "a", 2: "b"}}), [1]
        )

    def test_spel_run_func(self):
        interp = SpelInterpreter()

        def f(n):
            return n**2

        self.assertEqual(interp.run("#f(3)", {"f": f}), 9)

        class Stru:
            def __init__(self, offset=0):
                self.offset = offset

            def f(self, n):
                return n * 2 + self.offset

        self.assertEqual(interp.run("#a.f(3)", {"a": Stru()}), 6)

        def g(n):
            return Stru(n)

        self.assertEqual(interp.run("#g(3).f(5)", {"g": g}), 13)

        class Stru1:
            def f(self, n):
                return Stru(n)

        self.assertEqual(interp.run("#a.f(3).f(10)", {"a": Stru1()}), 23)

    def test_spel_run_inc_dec(self):
        interp = SpelInterpreter()

        def f(m, n):
            return m + n

        run_ctx = {"f": f, "a": 1, "b": 2}
        self.assertEqual(interp.run("#f(++#a, ++#b)", run_ctx), 5)
        self.assertEqual(run_ctx.get("a"), 2)
        self.assertEqual(run_ctx.get("b"), 3)

        run_ctx = {"f": f, "a": 1, "b": 2}
        self.assertEqual(interp.run("#f(#a++, #b++)", run_ctx), 3)
        self.assertEqual(run_ctx.get("a"), 2)
        self.assertEqual(run_ctx.get("b"), 3)

        run_ctx = {"f": f, "a": 1, "b": 2}
        self.assertEqual(interp.run("#f(--#a, --#b)", run_ctx), 1)
        self.assertEqual(run_ctx.get("a"), 0)
        self.assertEqual(run_ctx.get("b"), 1)

        run_ctx = {"f": f, "a": 1, "b": 2}
        self.assertEqual(interp.run("#f(#a--, #b--)", run_ctx), 3)
        self.assertEqual(run_ctx.get("a"), 0)
        self.assertEqual(run_ctx.get("b"), 1)

        run_ctx = {"b": 10}
        self.assertEqual(interp.run("#a=++#b", run_ctx), None)
        self.assertEqual(run_ctx.get("a"), 11)
        self.assertEqual(run_ctx.get("b"), 11)

        run_ctx = {"b": 10}
        self.assertEqual(interp.run("#a=#b++", run_ctx), None)
        self.assertEqual(run_ctx.get("a"), 10)
        self.assertEqual(run_ctx.get("b"), 11)

    def test_ast_run_safe_navi(self):
        interp = SpelInterpreter()

        class SubStru:
            def __init__(self):
                self.e = "hello"

        class Stru:
            def __init__(self, sub_stru=None):
                self.b = 4
                self.c = sub_stru

        self.assertEqual(interp.run("#a?.b", {"a": Stru()}), 4)
        self.assertEqual(interp.run("#a?.b", {"a": None}), None)
        self.assertEqual(interp.run("#a.b", {"a": Stru()}), 4)
        self.assertEqual(interp.run("#a?.c?.e", {"a": Stru()}), None)
        self.assertEqual(interp.run("#a?.c?.e", {"a": Stru(SubStru())}), "hello")

        run_ctx = {"l": []}
        self.assertEqual(interp.run("#l?.append('e')", run_ctx), None)
        self.assertTrue("e" in run_ctx.get("l"))

        self.assertEqual(interp.run("#l?.append('e')", {"l": None}), None)

    def test_spel_run_error(self):
        interp = SpelInterpreter()
        self.assertRaises(NameError, interp.run, "a=1")
        self.assertRaises(AttributeError, interp.run, "{1:'a',2:'b'}.![key1]")


if __name__ == "__main__":
    unittest.main()
